← ALL ARTICLES

GPU-Driven Rendering

全库目录221
本文目录22

GPU-Driven Rendering 把可见性、LOD、实例列表和绘制参数尽量留在 GPU 内生成。目的是避免 CPU 每帧逐对象提交和读回大规模可见列表。CPU 仍负责场景更新、资源与批次调度,GPU 负责生成可见列表和绘制参数。

专题阅读

  • Nanite几何选择与流送:Nanite 将高密度几何预处理为层级与分页数据,运行时按屏幕误差选择几何并流送细节。

CPU提交与批次规模

传统流程由 CPU:

  1. 遍历场景对象;
  2. 做视锥和遮挡剔除,选择LOD;
  3. 按材质和深度排序;
  4. 设置资源与状态;
  5. 发出绘制命令。

对象数量很大时,即使每个网格都很简单,渲染线程、驱动和状态管理也可能成为瓶颈。合批能减少绘制命令,但若多个对象共用一个可见性判断,就会降低剔除粒度;保持独立剔除又需要更细致的实例数据组织。

GPU驱动流程让CPU提交场景缓冲、相机和少量计算调度与间接绘制。GPU并行生成本帧可见工作集,后续绘制直接读取这个结果。省下的是逐对象提交和结果读回,剔除、压紧与同步则成为GPU上的新增工作。

场景数据

场景缓冲保存:

  • 实例的当前变换、上一帧变换和包围体;
  • 网格、LOD和材质标识;
  • 顶点、索引或几何簇数据;
  • 可见性与LOD历史;
  • 绘制和计算调度参数;
  • 可见实例或几何簇列表。

每个场景条目使用稳定标识,缓冲槽位由空闲列表和代数管理。玩法对象地址属于CPU地址空间,不能直接承担GPU索引和跨帧身份的职责。

场景更新与剔除批次

持久场景缓冲允许上传变化的实例区间,避免静止对象每帧重复传输。删除实例时先标记失效,待所有在途帧不再引用该槽位后才复用;否则上一帧的可见索引可能突然指向新对象。更新频繁的世界仍可能产生大量上传,增量更新的收益取决于变化比例和区间碎片。

多个通道需要相同的实例数据时,可以先收集剔除请求,再按兼容的视图、几何布局与输出格式集中执行。视图不同会改变视锥与屏幕误差,不能只因为使用同一个Shader就共享可见结果。Unreal GPUScene是引擎侧场景数据入口之一,具体缓冲和延迟剔除回调属于版本源码,本页仅解释这类组织方式的通用契约。

可见工作集

视锥与屏幕误差

计算着色器先测试包围球或轴对齐包围盒与视锥是否相交,再根据距离、投影尺寸或屏幕空间误差选择LOD。

LOD选择通常为进入和退出设置不同阈值,形成滞回区间,避免对象在边界附近来回切换。若精细LOD尚未驻留,就选择已经可用的粗层并发出请求,让本帧绘制继续执行。

背面锥剔除

一簇三角形可以预计算法线锥(Normal Cone),用锥轴和最大偏转角包住各面的法线。如果从相机方向判断,整个锥内的法线都背向视线,就能在光栅化前拒绝这簇三角形。透视视线在簇内会变化,保守测试还需要包围体和相机距离。

双面材质需要两侧可见,负缩放会翻转绕序,变形可能改变预计算法线范围。构建工具需要记录剔除适用条件,运行时根据实例变换和变形范围决定是否启用;条件不满足时保留几何。

Hi-Z遮挡剔除

深度金字塔的每级保存一块区域的保守深度。把包围体投影到屏幕后,选择对应的层级,并比较对象最近可能深度与所覆盖区域的Hi-Z值。

设正向深度近端为0、远端为1。一个投影矩形里的所有像素都比对象最近深度更近,才能确信对象被完全遮住;因此每级保存子区域的最大深度,只有objectNearest > tileMax + epsilon才拒绝。反向Z近端为1、远端为0,对应保存最小深度,并检查objectNearest < tileMin - epsilon。

例如正向深度区域为[0.2,0.7],对象最近深度0.6不能拒绝,因为深度0.7处仍可能看见它;最近深度0.9才满足完整遮挡。区域含背景1时,保守最大值为1,阻止误剔除。反向Z用背景0和最小值得到同样结论。epsilon按深度精度与投影误差设置,比较时偏向保留。

测试矩形要覆盖包围体投影的全部范围。选择某一级后,枚举或保守覆盖所有相交texel并归约,不能只采中心。包围体穿过近裁剪面时,直接投影角点可能产生无界或翻转矩形,应保留可见或先进行保守裁剪。

上一帧Hi-Z保存的是历史视图。相机移动、遮挡物移走或对象新出现时,历史遮挡关系可能已经失效。可采用以下保守措施:

  • 按运动范围扩张包围体;
  • 仅剔除有历史稳定遮挡的对象;
  • 先绘制上一帧可见集,生成当前深度;
  • 再测试不确定对象并补绘;
  • 对新实例和相机切换强制可见。

误判为遮挡会让物体从画面中消失。验证首先检查这种错误拒绝,再统计省下的工作量。

Hi-Z 也常称 HZB(Hierarchical Z-Buffer)。两者都指深度的分层降采样结构;归约使用最大值还是最小值取决于正向或反向 Z 以及遮挡测试定义。

遮挡归约与列表发布

遮挡判断的核心输入是保守投影矩形、对象最近深度和对应Hi-Z层,输出是能否确信对象完全被遮住。以下伪代码假定投影矩形已经裁到视口内,采用左上闭、右下开的像素边界;穿近裁剪面的对象由调用方直接保留。选定层级的每个纹素覆盖2^mip个基础像素,层内深度已经按前述正反Z规则归约。

text
IsFullyOccluded(pixelRect, nearestDepth, mip, reverseZ):
    scale = 2^mip
    first = floor(pixelRect.min / scale)
    end = ceil(pixelRect.max / scale)
    first、end分别夹到这一层的有效纹素边界
    if first.x >= end.x or first.y >= end.y: return false
    limit = 1 if reverseZ else 0
    for y from first.y to end.y - 1:
        for x from first.x to end.x - 1:
            d = HiZ[mip][x, y]       // 点读取,不做双线性过滤
            limit = min(limit, d) if reverseZ else max(limit, d)
    return nearestDepth < limit - epsilon if reverseZ
           else nearestDepth > limit + epsilon

向下取整起点、向上取整终点保证所有相交纹素都参与。较粗层可能连带覆盖矩形以外的像素,这只会减弱剔除,不应增加错误拒绝。Hi-Z生成器必须保证层级覆盖与边界一致;非二次幂尺寸可补齐到规定大小并用背景深度填充,或逐层保守处理边缘,不能在降采样时漏掉最后一行或一列。

确认可见后,线程用原子加取得可见列表槽位,槽位小于容量才写入实例标识。原子计数在容量耗尽后仍会增长,因此提交间接绘制以前还要限制最终计数并报告溢出。只对数组写入做边界判断,仍可能让间接绘制读取超出已写入的范围。生产系统需要前述保守回退,才能同时保证内存安全和画面完整。

发布参数和列表以后,分别建立到间接命令读取与Shader读取的同步。验证时逐项关闭剔除,比较计数和最终画面,并可视化投影矩形、选定层级和拒绝原因,才能区分投影错误、深度方向错误与容量丢失。

可见列表与绘制参数

列表压紧与前缀和

每个线程判断可见后,需要把结果紧凑写入可见列表。原子追加通过共享计数器取得槽位,流程简单,但大量线程同时追加时会争用计数器。另一种方法先写0/1标记,再计算前缀和得到输出偏移,最后按偏移散写。

例如可见标记为[1,0,1,1],排他前缀和为[0,1,1,2]。仅在标记为1的位置写入,因此实例0、2、3分别进入输出槽0、1、2,总数等于最后的前缀值加最后标记。分组实现先求组内前缀和与组总数,再对组总数扫描,最后把组偏移加到组内偏移。

原子追加不保证跨工作组的顺序,前缀扫描可保持输入顺序。后续需要透明排序时,稳定压紧仍不能替代按视图深度排序;它只避免压紧自己引入额外乱序。

间接绘制

GPU把可见数量和绘制参数写入间接参数缓冲,随后执行DrawIndexedIndirect、ExecuteIndirect或对应API。命令处理阶段直接读取缓冲中的参数,CPU因此无需为获得可见数量而等待GPU。

若每种网格和材质组合仍要一条间接命令,提交数量就取决于批次数量。多重间接绘制允许一次调用消费多条命令;无绑定资源(Bindless)则让Shader按索引选择资源,减少因绑定差异产生的批次。两者解决的是不同层面的开销。

间接参数从Shader写入切换为命令读取,需要相应屏障。计数溢出和缓冲容量不足也必须产生可观察的诊断,否则渲染错误可能只在场景规模扩大后出现。

绘制参数与两类消费者

例如场景实例ID为[10,11,12,13],标记[1,0,1,1]压紧后保存[10,12,13]。间接命令的实例数为3;顶点阶段收到局部实例序号1时,先查可见列表得到12,再访问实例12的变换。直接访问场景实例1会画到另一个对象的位置,即使计数和顶点数都完全正确。

单网格批次的索引参数可写成{indexCount, visibleCount, firstIndex, vertexOffset, firstInstance}。GPU生成的visibleCount对应实例数,索引数量仍来自该网格。顶点阶段按实例索引读取VisibleInstances,再查真正的场景变换;如果把压紧后的槽位直接当场景ID,对象位置就会错乱。

写完参数以后存在两类读取:间接命令阶段读取参数缓冲,顶点或网格阶段读取可见列表。以Vulkan同步2术语表示,Compute Shader写入需要分别对DRAW_INDIRECT / INDIRECT_COMMAND_READ和实际Shader阶段的SHADER_READ建立可见性。只给参数缓冲屏障,不能保证列表已经可读;跨队列时还涉及信号、等待与必要的所有权转移。

计数超过容量时,截断到容量能防止越界,却会丢对象。正确性路径应按已知场景上界预分配,或在溢出时改用保守的未剔除批次并发出诊断,再扩容后恢复。测试必须同时观察“是否越界”和“画面是否完整”。

材质分类与排序

可见几何需要按管线与材质组织。常见方式包括:

  • 每种材质使用独立分桶和计数器;
  • 对材质键做基数排序;
  • 可见性缓冲先记录图元标识,后续集中执行材质着色;
  • 无绑定描述符允许Shader根据材质标识访问资源。

例如可见实例[10,12,13]的材质键为[B,A,B],不透明路径可建立A桶[12]和B桶[10,13],分别生成实例数1和2的命令。桶内仍保存场景ID,不能因排序改用桶下标定位变换。若三者半透明且深度顺序为10、12、13,按材质改成12、10、13可能改变混合结果,此时深度顺序优先,或使用另行定义的透明算法。

无绑定访问降低了绑定次数,也让资源索引的有效期成为显式契约。随机读取增加缓存压力,同一线程束中材质差异大时,不同分支还会降低执行效率。

细粒度几何处理

几何簇与网格簇

几何簇(Cluster)和网格簇(Meshlet)把网格分成小块,并限制三角形与唯一顶点数量。每块保存包围体、法线锥、材质范围和局部索引。具体术语和上限由构建器与消费管线约定。

收益:

  • 在实例内部继续进行视锥、背面和遮挡剔除;
  • 将工作划成适合缓存和并行处理的数据块;
  • 可作为网格着色器的工作单元;
  • 可独立选择 LOD 和流送。

簇太小会增加元数据和调度,太大则减少细粒度剔除的收益。最佳大小取决于硬件、几何和工作负载,需要同时测量构建数据体积和运行开销。

Mesh Shader

网格着色管线可先由Task Shader或Amplification Shader产生工作组,再由Mesh Shader输出一组顶点和图元。前一级是否存在取决于API和具体管线。簇剔除、LOD选择与解码可以放在这些阶段内,几何数据由Shader主动读取。

Mesh Shader输出的图元仍交给硬件光栅器处理。软件光栅化则由计算着色器自行计算像素覆盖,并写入可见性与深度;两条路径需要分别评估成本。

平台不支持Mesh Shader时,可使用计算着色器剔除,再通过间接索引绘制消费结果。

可见性缓冲

可见性通道写入足以定位表面的一组数据,例如实例与三角形标识,必要时还保存重心坐标。后续计算或像素着色根据标识读取顶点与材质,重建表面属性并着色。例如像素记录实例12、三角形5及透视正确重心权重(0.2,0.3,0.5),三角形顶点UV为(0,0)、(1,0)、(0,1),则重建UV为(0.3,0.5)。实例与三角形ID先定位索引和顶点,重心权重再插值属性。若存的是屏幕线性权重,还需根据各顶点clip.w做透视校正,不能直接代入上式,相关关系见 光栅化与属性插值。具体保存哪些字段,取决于重建成本、精度和缓冲格式。

优势:

  • 几何可见性阶段可以使用较简单的Shader;
  • 被覆盖的几何不重复执行完整材质;
  • 材质计算可按屏幕块或材质分类;
  • 能与微三角形和GPU生成的可见列表配合。

代价:

  • 需要随机读取索引、顶点和材质;
  • 属性导数、纹理LOD和MSAA处理更复杂;
  • 透明材质仍需其他路径;
  • 标识编码和缓冲带宽需要单独预算。

验证方法

  • 统计输入实例与簇数量、各级剔除后的数量和最终绘制或计算调度规模。
  • 输出视锥、背面、遮挡和LOD的拒绝原因。
  • 测试相机切换、快速移动、新生对象和遮挡物移走。
  • 检查间接参数、屏障、可见列表容量和溢出处理。
  • 分别记录CPU渲染线程,以及GPU剔除、光栅化、着色与流送的时间。
  • 改变三角形的屏幕尺寸,区分图元建立、覆盖计算、像素着色和原子写入成本。
  • 对缺页回退、LOD突跳、裂缝、遮罩材质和运动向量做回归。

参考实验与证据

CPU参考模型的occluded检查正反Z、背景保留、深度相等和完整区域归约。2026-10-22执行python 知识库/examples/revision_22_26/test_reference.py,21项通过,其中4项属于遮挡关系。它仅在已经收集完整深度样本、epsilon=0的条件下验证标量比较,不覆盖投影矩形、层级坐标、GPU原子、跨队列屏障或绘制输出。正文使用机制伪代码,GPU编译、运行与性能尚未验证。

相关主题

参考资料

  • Khronos,VkDrawIndexedIndirectCommand,Members与Valid Usage;2026-10-22核读。五个参数及实例起点能力限制由此核对。
  • Brian Karis、Rune Stubbe、Graham Wihlidal,Nanite: A Deep Dive,PDF第75页Two-Pass Occlusion Culling;2026-10-22核读,说明历史HZB后的当前帧复查。正反Z标量关系由本页推导和参考实验支持。
  • 通用同步语义见GPU同步与屏障;Nanite的组层级和流送条件见Nanite几何选择与流送。